모노레포 배포 분리하기 재도전기
[[2. Area/(W) 헬퍼쿡&헬퍼프라이 - 유지보수/모노레포 버전 도입 재도전기|모노레포 버전 도입 재도전기]]를 진행하다가, "버전 도입"을 버리기로 하고, 배포 분리만 살림.
결과적으로 "버전 관리"를 버리니, "changeset"을 같이 사용할 이유가 없어졌다.
- 버전 관리는 이번 목표에서 스킵. 여기서 버전 관리까지 하려니까 너무 꼬임.
- changeset을 활용하는 목적은 "변경 사항 감지"에 그칠 것. 근데 이거 turborepo 자체로 확인하는 법 없나?
- 있다고 함
- turborepo는 현재 브랜치와 비교 대상(예: main 또는 이전 커밋)을 비교하여 영향을 받는 프로젝트만 필터링할 수 있음.
# develop 브랜치와 비교해서 변경한 것과, 그것에 의존하는 앱만 빌드
pnpm turbo build --filter=...[origin/develop]
...[origin/develop]: "origin/develop 브랜치 이후로 변경된 모든 것과 그 변경사항에 의존하는 하위 프로젝트들"을 의미합니다.- 이 명령어를 실행하면, 변경되지 않은 앱은 자동으로
>>> FULL TURBO(캐시 히트) 가 뜨거나 실행 대상에서 제외됩니다.
- 그렇담 이제 changeset을 유지해야 할 이유가 없음. changeset은 업데이트를 해야 할 패키지를 명시적으로 선택할 수 있는 부분에서 낫지만, 버전 관리를 하지 않는다면 이 기능도 필요 없음.
- 이 명령어를 실행하면, 변경되지 않은 앱은 자동으로
- 나의 의문점 두 가지
- 만약 apps/helper-cook과 packages/ui가 같이 변경이 됐다고 치자. 그럼 paciages/ui를 사용하고 있는 apps/helper-fry에서 에러가 나는가 ?
- CI (빌드) 단계에서 잡아낼 수 있다. 모노레포를 사용하는 이유 중 하나. 공통 코드를 수정했을 때 영향 받는 모든 앱을 배포 전 단계에서 미리 검증할 수 있다.
- 그래서 push 전에 로컬에서 CI 단계를 미리 수행하는 단계가 필요하다.
-
husky & lint-staged 활용: eslint, prettier, tsc -b
-
husky 활용, git push 할 때 (pre-push) 자동으로 검사 스크립트가 돌아가게 설정하기.
```bash
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"echo "🚀 Push 전 영향 받는 프로젝트 검증 시작..." # 1. 빌드 및 타입 체크 (영향 받는 모든 프로젝트) pnpm turbo build --filter=...[origin/main] # 2. 린트 체크 pnpm turbo lint --filter=...[origin/main] ```
-
- apps/helper-fry의 코드가 변경되지 않았더라도, apps/helper-fry에서 packages/ui를 사용하기 때문에 turborepo가 apps/helper-fry도 같이 변경해야 한다고 알려주는가?
- 그렇다.
pnpm turbo build --filter=...[origin/main]: **"변경된 것과, 그것에 의존하는 모든 것"**을 찾아냅니다.packages/ui가 변경됨.helper-fry는 코드가 안 바뀌었지만,packages/ui에 의존함.- 결과: Turborepo는
helper-fry도 빌드 대상에 포함시킵니다. (UI가 바뀌었으니 앱도 다시 빌드해서 새 UI를 반영해야 하기 때문)
- 그렇다.
- 만약 apps/helper-cook과 packages/ui가 같이 변경이 됐다고 치자. 그럼 paciages/ui를 사용하고 있는 apps/helper-fry에서 에러가 나는가 ?
시나리오 테스트 하기
환경 정리
apps/project-a
apps/project-b
packages/eslint-config
packages/prettier-config
packages/react-hooks
packages/typescript-config
packages/ui
시나리오 1. feature 개발 -> develop에 배포 (production 서버 배포 X, develop 서버 배포 O) 했을 때, 관련된 것만 빌드하기
시나리오 2. develop에 업로드 된 커밋을 main 브랜치에 올려서 production 서버에 배포하기
현재 프로젝트 환경을 main, develop 브랜치로 나누어 관리하고 있기 때문에, 비교 기준을 분리해야 함.
- develop 브랜치에서: "마지막으로 성공적으로
develop에 배포된 시점" 이후의 변경분만 배포해야 합니다. - main 브랜치에서: "마지막으로 성공적으로
main에 배포된 시점" 이후의 변경분(주로develop에서 넘어온 것들)만 배포해야 합니다.
테스트 환경 설정
-
pnpm-workspace.yaml과 각 앱의package.json에서 의존성이 잘 설정되어 있어야 함.-
pnpm-workspace.yamlpackages: - "apps/*" - "packages/*" -
package의
package.json의 name을 확인하고, 사용하는 쪽의package.json의 dependency에 포함되어 있는지 확인{ "name": "project-a", "dependencies": { "@test/ui": "workspace:*" // workspace 프로토콜을 사용하면 로컬 패키지를 바라봅니다. } }
-
Github Actions 워크플로우 구성
.github/workflows/deploy.yml
- trigger 확인
-
trigger는 PR closed 상황으로 제한함.
-
develop에 직접 push 했을 때 배포되는 상황을 막기 위함.
- develop에 직접 push 하는 상황을 최대한 안 만들고, 이를 실수라고 여긴다는 전제.
-
main에 hotfix 등으로 반영된 상황이 있고, 이를 develop에 반영할 때 개발 서버가 배포되는 상황 방지
-
-
원격으로 수동 실행하는 경우가 있기 때문에
workflow_dispatch옵션 추가
on:
pull_request:
types: [closed]
branches:
- main
- develop
workflow_dispatch: # 수동 실행 버튼 추가
-
변경사항 감지 및 빌드
-
이 단계에서 "영향을 받는 패키지를 확인하고, 해당 패키지를 빌드하는 과정"을 수행합니다.
-
조건부 실행
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true-
workflow_dispatch즉 수동 배포를 실행했거나, -
실제로 코드가 타겟 브랜치에 병합되었을 때만 로직을 실행합니다.
-
-
비교 기준점 설정
if [[ "${{ github.event_name }}" == "pull_request" ]]; then echo "BASE=HEAD^1" >> $GITHUB_ENV-
Squash Merge를 사용하는 전제 하에, 병합이 완료된 직후입니다. 따라서 **현재 내 위치(HEAD)**는 모든 기능이 합쳐진 커밋이고, **
HEAD^1**은 합쳐지기 바로 직전의 타겟 브랜치 상태입니다. -
이 둘을 비교하여 이번 PR을 통해 들어온 모든 수정사항을 추출합니다.
-
-
빌드
run: pnpm turbo build --filter=...[${{ env.BASE }}]--filter: 전체 프로젝트 중 이번 수정 사항에 영향을 받는 프로젝트만 골라냅니다....[${{ env.BASE }}]:BASE이후로 바뀐 소스 코드를 찾습니다.[...]대괄호 안의 기준점(커밋 SHA, 브랜치명 등)과 현재 상태를 비교합니다....(Tribble Dots): 그 소스를 사용하는 하위 패키지(ex.packages/ui등)가 있다면, 그 패키지에 의존하는 상위 앱들(project-a,project-b)까지 빌드 대상으로 포함합니다.
- 결과적으로 수정되지 않은 프로젝트를 건드리지 않고, 꼭 필요한 것만 빌드합니다.
-
배포 대상 전달
-
이제 어떤 패키지가 빌드 대상에 포함되었는지 다음 Job로 넘겨야 합니다. 그래야 어떤 패키지를 배포해야 할지 판별하기 때문입니다.
PLAN=$(pnpm turbo build --filter=...[${{ env.BASE }}] --dry=json) -
해당 코드를 통해,
pnpm turbo build --filter=...[${{env.BASE}}] --dry=json에 대한 JSON 형식의 결과가PLAN변수에 담깁니다. 이 결과를 통해 어떤
-
-
-
시나리오 1: Feature 개발 -> 개발 환경 배포
- 로컬 작업:
apps/project-a의 코드를 수정하거나,packages/ui를 수정합니다. - Push 전 검증: 로컬에서
husky가pnpm turbo build --filter=...[origin/main]를 실행하여project-a가 깨지지 않는지 확인합니다. - develop 브랜치 Push:
- GitHub Actions가 실행됩니다.
build-and-testJob에서project-a만 빌드됩니다. (캐싱 활용)deploy-devJob이 실행되지만,project-b는 변경사항이 없으므로 스킵되고project-a만 배포됩니다.- 결과:
main브랜치는 영향이 없고,develop서버만 업데이트됩니다.
내가 한 실제 행동
-
packages/ui코드 수정 -
pnpm turbo build --filter=...[origin/main] 실행-
결과:

-
결과 분석
-
현상 의미 배포 여부 판단 Cache Miss 코드가 바뀌어서 새로 빌드함. 배포 필요 Cache Hit 코드는 예전 어떤 시점과 같음. 그래서 복사본을 꺼내줌. 상황에 따라 다름 -
공통 패키지 UI만 변경했을 때,
- 사용하지 않는 react-hooks -> cache hit -> 빌드 안 함
- 사용하는 project-a, project-b, 그리고 packages/ui -> cache miss -> 새로 빌드 함
-
-
-
project-a코드만 수정했을 때는 다른 프로젝트는 변경 사항이 없으므로, 모두 cache hit이 나야 함 -> 확인 -
project-a만 수정한 경우 -> project-a 만 서버 배포되는 로직 실행, project-b는 실행 안됨 확인
-
project-b만 수정한 경우 -> project-b만 서버 배포되는 로직 실행, project-a는 실행 안 됨 확인
-
ui만 수정한 경우 -> project-a, project-b 둘 다 서버 배포되는 로직 실행 확인
시나리오 2: 개발 환경 -> 운영 환경 배포
개발 환경에서 운영 환경으로 배포할 때의 흐름
- develop 브랜치에서 main 브랜치를 타겟으로 하는 PR을 연다.
트러블슈팅 1. 직전 커밋이 Merge ... 인 경우, 변경 사항을 감지하지 않고 아무 프로젝트도 배포하지 않음.
원인: BASE=HEAD^1의 설정
- 상황:
develop에서 기능을 다 만들고main으로 합쳤습니다. (예: 커밋 5개가 합쳐짐) - 현재 로직:
main에 코드가 들어오자마자 **직전 커밋(HEAD^1)**과 비교합니다. - 결과: 만약
main브랜치의 마지막 커밋이 "Merge pull request..." 같은 머지 커밋이라면, 터보는 그 머지 커밋 하나만 보고 "어? 바뀐 게 별로 없네?"라고 판단하거나, 반대로 예상치 못한 기준으로 빌드 범위를 잡습니다.
즉, 운영 배포(main)는 '직전 커밋'이 아니라 '마지막으로 성공했던 운영 배포 시점'과 비교해야 합니다.
- name: Determine Base Ref
id: set-base
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
# main 배포 시: github.event.before(이전 상태)를 가져옴
BASE_REF="${{ github.event.before }}"
# 만약 첫 배포라 'before'가 0000... 이라면 안전하게 HEAD^1 사용
if [ "$BASE_REF" = "0000000000000000000000000000000000000000" ] || [ -z "$BASE_REF" ]; then
BASE_REF="HEAD^1"
fi
echo "BASE=$BASE_REF" >> $GITHUB_ENV
else
# develop 배포 시: 항상 직전 커밋과 비교
echo "BASE=HEAD^1" >> $GITHUB_ENV
fi
-
BASE_REF="${{ github.event.before }}"- 현재 push가 일어나기 직전의 최신 커밋 해시(ID), "비교의 시작점"으로 사용함.
- 반드시 squash merge를 해야 의미 있게 됨?
- 현재 push가 일어나기 직전의 최신 커밋 해시(ID), "비교의 시작점"으로 사용함.
-
0000.../-z의미
0000...: 브랜치를 새로 만들고 처음으로 push 했을 때 Github가 보내는 값. "이전 기록이 전혀 없다"는 뜻.-z "$BASE_REF": 변수가 비어있는 경우를 대비한 안전장치.